#include "date.h"
#include "logger.h"
#include <string.h>
#include <chrono>

OPEN_JLIB_NS


const s32 Date::DaysOfYear[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
const s32 Date::DaysOfMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};


s64 Date::currentSeconds () noexcept {
  auto tp = std::chrono::system_clock::now();
  return std::chrono::duration_cast<std::chrono::seconds>(tp.time_since_epoch()).count();
}
s64 Date::currentMilliseconds () noexcept {
  auto tp = std::chrono::system_clock::now();
  return std::chrono::duration_cast<std::chrono::milliseconds>(tp.time_since_epoch()).count();
}
s64 Date::currentMicroseconds () noexcept {
  auto tp = std::chrono::system_clock::now();
  return std::chrono::duration_cast<std::chrono::microseconds>(tp.time_since_epoch()).count();
}
s64 Date::currentNanoseconds () noexcept {
  auto tp = std::chrono::system_clock::now();
  return std::chrono::duration_cast<std::chrono::nanoseconds>(tp.time_since_epoch()).count();
}
s32 Date::currentTimezone () noexcept {
  time_t utc = 0L;
  auto tm = localtime(&utc);
  return tm->tm_hour > 12 ? tm->tm_hour - 24 : tm->tm_hour;
}


Date::Date () noexcept {
  _z = (s8)currentTimezone();
  set(currentSeconds());
}
Date::Date (s64 utc) noexcept {
  _z = (s8)currentTimezone();
  set(utc);
}
Date::Date (s64 utc, s32 timezone) noexcept {
  debug_assert(timezone >= MinTimezone && timezone <= MaxTimezone);
  _z = (s8)timezone;
  set(utc);
}


void Date::set (s64 utc) noexcept {
  if (_u == utc)
    return;
  _u = utc;
  _l = utc + EpochSecondOffset + _z*SecondsPerHour;
  auto t = _l;
  _s = (s8)(t % SecondsPerMinute);
  t /= SecondsPerMinute;
  if (_s < 0) {
    _s += SecondsPerMinute;
    t -= 1;
  }
  _m = (s8)(t % MinutesPerHour);
  t /= MinutesPerHour;
  if (_m < 0) {
    _m += MinutesPerHour;
    t -= 1;
  }
  _h = (s8)(t % HoursPerDay);
  t /= HoursPerDay;
  if (_h < 0) {
    _h += HoursPerDay;
    t -= 1;
  }
  _w = (s8)((EpochDayOfWeek*SecondsPerWeek + t) % DaysPerWeek);
  auto y400 = t / DaysPer400Year;
  auto d400 = t - DaysPer400Year*y400;
  if (d400 < 0) {
    d400 += DaysPer400Year;
    y400 -= 1;
  }
  auto y100 = d400 / DaysPer100Year;
  if (y100 == 4) {
    _Y = (s32)(400*y400 + 399);
    _D = DaysPerYear;
    _M = 11;
    _d = 30;
    return;
  }
  auto d100 = d400 - DaysPer100Year*y100;
  auto y4 = d100 / DaysPer4Year;
  auto d4 = d100 - DaysPer4Year*y4;
  auto y = d4 / DaysPerYear;
  if (y == 4) {
    _Y = (s32)(400*y400 + 100*y100 + 4*y4 + 3);
    _D = DaysPerYear;
    _M = 11;
    _d = 30;
    return;
  }
  _Y = (s32)(400*y400 + 100*y100 + 4*y4 + y);
  auto D = _D = (s16)(d4 - DaysPerYear*y);
  if (y == 3 && (y4 != 24 || y100 == 3) && D >= 59) {
    if (D == 59) {
      _M = 1;
      _d = 28;
      return;
    }
    D -= 1;
  }
  for (_M = 11; _M > 0; --_M) {
    if (D >= DaysOfYear[_M])
      break;
  }
  _d = (s8)(D - DaysOfYear[_M]);
}
bool Date::setTime (s32 hour, s32 minute, s32 second) noexcept {
  if (hour < 0 || hour > 23)
    return false;
  if (minute < 0 || minute > 59)
    return false;
  if (second < 0 || second > 59)
    return false;
  auto os = SecondsPerHour*_h + SecondsPerMinute*_m + _s;
  auto ns = SecondsPerHour*hour + SecondsPerMinute*minute + second;
  auto ds = ns - os;
  _l += ds;
  _u += ds;
  _h = (s8)hour;
  _m = (s8)minute;
  _s = (s8)second;
  return true;
}
bool Date::setDate (s32 year, s32 month, s32 day) noexcept {
  if (year == 0)
    return false;
  if (month < 1 || month > 12)
    return false;
  if (day < 1)
    return false;
  auto y = year > 0 ? year-1 : year;
  auto y400 = y / 400;
  y %= 400;
  if (y < 0) {
    y += 400;
    y400 -= 1;
  }
  auto y100 = y / 100;
  y %= 100;
  auto y4 = y >> 2;
  y &= 3;

  auto m = month - 1;
  auto d = day - 1;
  auto leap = y == 3 && (y4 != 24 || y100 == 3);
  if (leap && m == 1) {
    if (d >= 29)
      return false;
  }
  else if (d >= DaysOfMonth[m])
    return false;
  _Y = 400*y400 + 100*y100 + 4*y4 + y;
  _D = (s16)(DaysOfYear[m] + d);
  _M = (s8)m;
  _d = (s8)d;
  if (leap && m > 1)
    _D += 1;
  _l = SecondsPer400Year*y400 + SecondsPer100Year*y100 + SecondsPer4Year*y4 + SecondsPerYear*y;
  _l += SecondsPerDay*_D + SecondsPerHour*_h + SecondsPerMinute*_m + _s;
  _u = _l - SecondsPerHour*_z;
  return true;
}
bool Date::setDateTime (s32 year, s32 month, s32 day, s32 hour, s32 minute, s32 second) noexcept {
  return setDate(year, month, day) && setTime(hour, minute, second);
}
bool Date::setTimezone (s32 timezone) noexcept {
  if (timezone < MinTimezone || timezone > MaxTimezone)
    return false;
  auto dz = timezone - _z;
  if (dz == 0)
    return true;
  set(_u + SecondsPerHour*dz);
}


Date& Date::addSeconds (s32 value) noexcept {
  set(_u + value);
  return *this;
}
Date& Date::addMinutes (s32 value) noexcept {
  set(_u + SecondsPerMinute*value);
  return *this;
}
Date& Date::addHours (s32 value) noexcept {
  set(_u + SecondsPerHour*value);
  return *this;
}
Date& Date::addDays (s32 value) noexcept {
  set(_u + SecondsPerDay*value);
  return *this;
}
Date& Date::addWeeks (s32 value) noexcept {
  set(_u + SecondsPerWeek*value);
  return *this;
}
Date& Date::addMonths (s32 value) noexcept {
  s64 dy = value / 12;
  s64 dm = value % 12;
  s64 m = _M + dm;
  if (m < 0) {
    m += 12;
    dy -= 1;
  }
  else if (m > 11) {
    m -= 12;
    dy += 1;
  }
  auto y = _Y + dy;
  auto y4 = y & 3;
  if (y4 < 0)
    y4 += 4;
  auto y100 = y % 100;
  if (y100 < 0)
    y100 += 100;
  auto y400 = y % 400;
  if (y400 < 0)
    y400 += 400;
  auto leap = y4 == 3 && (y100 != 99 || y400 == 399);
  if (leap && m == 1) {
    if (_d > 28)
      _d = 28;
  }
  else if (_d > DaysOfMonth[m] - 1)
    _d = (s8)(DaysOfMonth[m] - 1);
  _Y = (s32)y;
  _M = (s8)m;
  _D = (s16)(DaysOfYear[m] + _d);
  if (leap && m > 1)
    _D += 1;
  return *this;
}
Date& Date::addYears (s32 value) noexcept {
  s64 y = _Y + value;
  auto y4 = y & 3;
  if (y4 < 0)
    y4 += 4;
  auto y100 = y % 100;
  if (y100 < 0)
    y100 += 100;
  auto y400 = y % 400;
  if (y400 < 0)
    y400 += 400;
  auto leap = y4 == 3 && (y100 != 99 || y400 == 399);
  auto m = (s64)_M;
  auto d = (s64)_d;
  if (leap && m == 1) {
    if (d > 28)
      d = 28;
  }
  else if (d > DaysOfMonth[m] - 1)
    d = DaysOfMonth[m] - 1;
  _Y = (s32)y;
  _D = (s16)(DaysOfYear[m] + d);
  _M = (s8)m;
  _d = (s8)d;
  if (leap && m > 1)
    _D += 1;
  return *this;
}


CLOSE_JLIB_NS
